package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    @Override
    public Result queryById(Long id) {
        //缓存穿透
        Shop shop = cacheClient.queryWithPassThrough(Shop.class,RedisConstants.CACHE_SHOP_KEY,id,this::getById,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
        //互斥锁解决缓存击穿
        //Shop shop = queryWithMute(id);
        //逻辑过期解决缓存击穿
       // Shop shop1=cacheClient.queryWithLogicalExpeire(id,RedisConstants.CACHE_SHOP_KEY,Shop.class,this::getById,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
        if (shop == null){
            return Result.fail("店铺信息不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 逻辑过期封装缓存击穿
     * @param id
     * @return
     */
//    public Shop queryWithLogicalExpeire(Long id) {
//        //1.根据id 在Redis中查询商铺缓存
//        String key = RedisConstants.CACHE_SHOP_KEY + id;
//        String shopJson = stringRedisTemplate.opsForValue().get(key);
//        //2.判断是否命中缓存
//       if (StrUtil.isBlank(shopJson)){
//           //3.未命中，返回空
//           return null;
//       }
//        //4.命中缓存，需要吧JSON字符串反序列化为java对象
//        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
//       //这里直接定义的data是object类型，但是取出来的时候其实是JSONObject类型，因此可以强转。再转成java对象
//        JSONObject data = (JSONObject) redisData.getData();
//        Shop shop = JSONUtil.toBean(data, Shop.class);
//        LocalDateTime expireTime = redisData.getExpireTime();
//        //5.判断缓存是否过期
//        if(expireTime.isAfter(LocalDateTime.now())){
//            //5.1未过期，返回商户信息
//            return shop;
//        }
//        //5.2已过期，尝试获取互斥锁
//        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
//        boolean isLock = tryLock(lockKey);
//        //6.判断是否获取互斥锁
//        if (isLock){
//            //6.1成功获取，(二次检验逻辑时间是否过期)开启独立线程
//            CACHE_REBUILD_EXECUTOR.submit(()->{
//                try {
//                    //重建缓存
//                    this.saveShopToRedis(id,20L);
//                } catch (Exception e) {
//                    throw new RuntimeException(e);
//                }finally {
//                    //释放锁
//                    unlock(lockKey);
//                }
//            });
//        }
//        //6.2未成功，返回过期商铺数据
//        return shop;
//    }

    /**
     * 互斥锁封装缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithMute(Long id) {
        //1.根据id 在Redis中查询商铺缓存
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //2.判断是否命中缓存
        Shop shop = getShopFromCache(key);
        //3.未命中缓存
        //3.1尝试申请互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
        try {
            boolean isLock = tryLock(lockKey);
            //3.2申请不成功，休眠一段时间并再次去缓存中尝试命中
            if(!isLock){
                Thread.sleep(50);
                queryWithMute(id);
            }
            //3.3申请成功，先去缓存看看能不能获取数据了，不能再根据id查询数据库
           getShopFromCache(key);
            shop = getById(id);
            //模拟延时重建
            Thread.sleep(200);
            //4.数据库无商户
            if(shop == null){
                //向缓存中存入一个null数据
                stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return  null;
            }
            //5.数据库有商户，将商户信息添加到缓存
            String jsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(key,jsonStr,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //6.释放锁
            unlock(lockKey);
        }
        //返回商户信息
        return shop;
    }
    /**
     * 封装缓存穿透问题
     * @param id
     * @return
     */
//    public Shop queryWithPassThrough(Long id) {
//        //1.根据id 在Redis中查询商铺缓存
//        String key = RedisConstants.CACHE_SHOP_KEY + id;
//        //2.判断是否存在
//        getShopFromCache(key);
//        //3.未查到缓存，查询数据库
//        //注意，这里因为已经继承了由myba-plus的ServiceImpl<ShopMapper, Shop>
//        //因此可以直接使用其中方法
//        Shop shop = getById(id);
//        //4.数据库无商户
//        if(shop == null){
//            //向缓存中存入一个空值数据
//            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
//            return  null;
//        }
//        //5.数据库有商户，将商户信息添加到缓存
//        String jsonStr = JSONUtil.toJsonStr(shop);
//        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,jsonStr,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        //返回商户信息
//        return shop;
//    }

    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 在缓存中获取数据并判断空值
     * @param key
     * @return
     */
    private Shop getShopFromCache(String key){
        String shopJSON = stringRedisTemplate.opsForValue().get(key);
        //2.存在，直接返回(这里只有有值时才能返回ture，其他情况都是false)
        //但是false还有两种情况，1)完全为空(这里是null)——要去数据库查询
        // 2)已经查完数据库返回""在缓存里了——要去判断(这里是“”空字符串)
        //判断是不是null：Objects.nonNull(shopJson)，不是null说明是缓存的“”，报错
        //判断是不是“”："".equals(shopJson)，仅当 shopJson 是空字符串 "" 时才返回 true，此时报错
        if(StrUtil.isNotBlank(shopJSON)){
            return JSONUtil.toBean(shopJSON, Shop.class);
        }
        if(shopJSON != null){
            return null;
//            return Result.fail("店铺信息不存在");
        }
        //既没有值，也没有空字符
        return null;
    }

    @Override
    public LambdaUpdateChainWrapper<Shop> lambdaUpdate() {
        return super.lambdaUpdate();
    }

    /**
     * 将shop数据和逻辑过期时间一起存入到redis当中
     * @param id
     */
     public void saveShopToRedis(Long id,Long expireSeconds){
        //1.获取店铺数据
        Shop shop = getById(id);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.存入到redis当中
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    /**
     *缓存更新策略-先更新数据库，再删除缓存
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {
        //1.更新数据库数据
        updateById(shop);
        //2.删除缓存数据
        Long id = shop.getId();
        if(id != null){
            stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+id);
            return Result.ok();
        }
        return Result.fail("店铺id不能为空");
    }
}
